﻿namespace Atomic.Plugins.Files
{
    using System;
    using System.IO;
    using System.Reflection;
    using System.Web.Hosting;

    using Atomic.Logging;

    /// <summary>
    /// 程序集探测目录。
    /// </summary>
    public class DefaultAssemblyProbingFolder : IAssemblyProbingFolder
    {
        /// <summary>
        /// 探测目录名。
        /// </summary>
        private const string BasePath = "Dependencies";

        /// <summary>
        /// AppData 的虚拟目录。
        /// </summary>
        private const string AppDataPath = "~/App_Data";

        /// <summary>
        /// 日志。
        /// </summary>
        private readonly ILogger _logger = null;

        /// <summary>
        /// 程序集加载器。
        /// </summary>
        private readonly IAssemblyLoader _assemblyLoader = null;

        /// <summary>
        /// 初始化默认程序集探测目录。
        /// </summary>
        /// <param name="assemblyLoader">程序集加载器。</param>
        public DefaultAssemblyProbingFolder(IAssemblyLoader assemblyLoader)
        {
            this._assemblyLoader = assemblyLoader;

            this._logger = LoggerFactory.Current.Create();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public bool AssemblyExists(string name)
        {
            var path = this.PrecompiledAssemblyPath(name);

            return File.Exists(CombineToPhysicalPath(path));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="moduleName"></param>
        /// <returns></returns>
        public string GetAssemblyVirtualPath(string moduleName)
        {
            var path = PrecompiledAssemblyPath(moduleName);
            if (!File.Exists(CombineToPhysicalPath(path)))
                return null;

            return Path.Combine(AppDataPath, path).Replace(Path.DirectorySeparatorChar, '/');
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public Assembly LoadAssembly(string name)
        {
            if (!this.AssemblyExists(name))
                return null;

            return _assemblyLoader.Load(name);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        public void DeleteAssembly(string name)
        {
            var path = CombineToPhysicalPath(PrecompiledAssemblyPath(name));

            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }

        /// <summary>
        /// 保存程序集。
        /// </summary>
        /// <remarks>
        /// 将指定名称的程序集复制到 fileName 位置。
        /// </remarks>
        /// <param name="name">程序集名称。</param>
        /// <param name="fileName">保存位置。</param>
        public void StoreAssembly(string name, string fileName)
        {
            var path = PrecompiledAssemblyPath(name);

            //获得程序集的实际物理路径。
            var destinationFileName = CombineToPhysicalPath(path);

            //获得程序集所在位置的目录。
            var dependenciesFolder = Path.Combine(this.GetRootFolder(), BasePath.Replace('/', Path.DirectorySeparatorChar));

            //目录不存在时创建目录。
            if (!Directory.Exists(dependenciesFolder))
            {
                Directory.CreateDirectory(dependenciesFolder);
            }

            if (File.Exists(destinationFileName))
            {
                File.Delete(destinationFileName);
            }

            this.RemoveLockFile(destinationFileName);

            File.Copy(fileName, destinationFileName);
        }

        /// <summary>
        /// 获得程序集最后写入的 UTC 时间。
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public DateTime GetAssemblyDateTimeUtc(string name)
        {
            return File.GetLastWriteTimeUtc(CombineToPhysicalPath(PrecompiledAssemblyPath(name)));
        }

        private string PrecompiledAssemblyPath(string name)
        {
            return Path.Combine(BasePath, name + ".dll").Replace(Path.DirectorySeparatorChar, '/');
        }

        private string CombineToPhysicalPath(params string[] paths)
        {
            return this.ValidatePath(this.GetRootFolder(), Path.Combine(this.GetRootFolder(), Path.Combine(paths)).Replace('/', Path.DirectorySeparatorChar));
        }

        private string ValidatePath(string basePath, string mappedPath)
        {
            bool valid = false;

            try
            {
                // Check that we are indeed within the storage directory boundaries
                valid = Path.GetFullPath(mappedPath).StartsWith(Path.GetFullPath(basePath), StringComparison.OrdinalIgnoreCase);
            }
            catch
            {
                valid = false;
            }

            if (!valid)
            {
                throw new ArgumentException("Invalid path");
            }

            return mappedPath;
        }

        private string GetRootFolder()
        {
            return HostingEnvironment.MapPath(AppDataPath);
        }

        /// <summary>
        /// 移除锁定文件。
        /// </summary>
        /// <param name="destinationFileName">文件完整名称。</param>
        /// <returns></returns>
        private void RemoveLockFile(string destinationFileName)
        {
            if (!File.Exists(destinationFileName))
                return;

            const string extension = "deleted";
            for (int i = 0; i < 100; i++)
            {
                var newExtension = (i == 0 ? extension : string.Format("{0}{1}", extension, i));
                var newFileName = Path.ChangeExtension(destinationFileName, newExtension);
                try
                {
                    File.Delete(newFileName);
                    File.Move(destinationFileName, newFileName);

                    // If successful, we are done...
                    return;
                }
                catch
                {
                    // We need to try with another extension
                }
            }

            // Try again with the original filename. This should throw the same exception
            // we got at the very beginning.
            try
            {
                File.Delete(destinationFileName);
            }
            catch (Exception e)
            {
                this._logger.LogError("删除“{0}”文件失败。{1}", destinationFileName, e.Message);
            }
        }
    }
}
